Skip to content

Dialog/Drawer: lock scroll on the root element to prevent layout shift#42545

Merged
mdo merged 2 commits into
v6-devfrom
mdo/dialog-root-scroll-lock
Jun 27, 2026
Merged

Dialog/Drawer: lock scroll on the root element to prevent layout shift#42545
mdo merged 2 commits into
v6-devfrom
mdo/dialog-root-scroll-lock

Conversation

@mdo

@mdo mdo commented Jun 23, 2026

Copy link
Copy Markdown
Member

Problem

Opening a dialog/drawer can shift page content sideways, and with scrollbar-gutter: stable the reserved gutter renders as a bare white strip instead of the dimmed overlay (#40659).

Cause

scrollbar-gutter: stable is on :root (_root.scss), so the viewport scrollbar and its reserved gutter live on <html>. But the scroll-lock applied overflow: hidden to <body>. Because <html>'s overflow is visible, the body→viewport overflow-propagation quirk hides the scrollbar at the viewport level while the gutter reservation stays on <html> — and in that split state browsers don't reliably honor the gutter, so content reflows and the gutter strip falls outside the native ::backdrop.

Fix

Apply the dialog-open lock to the root element (<html>), co-locating overflow: hidden with scrollbar-gutter: stable. The gutter stays reserved while the scrollbar hides (no shift), and since <html> is the viewport scroller the ::backdrop covers the gutter (no white strip).

  • dialog-base.js: toggle dialog-open on document.documentElement instead of document.body.
  • _dialog.scss: scope the rule to :root.dialog-open.
  • Tests + migration note updated.

Fixes #39221, fixes #39972, fixes #40908, fixes #40659.

Apply the dialog-open scroll-lock (overflow: hidden) to the root <html>
element instead of <body>, so it sits on the same element as
scrollbar-gutter: stable. Co-locating them keeps the gutter reserved
while the scrollbar is hidden, so the page no longer shifts (and the
::backdrop covers the gutter instead of leaving a white strip) when a
dialog or drawer opens. Fixes #39221, #39972, #40908, #40659.
@mdo mdo requested review from a team as code owners June 23, 2026 23:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR changes how Bootstrap 6’s Dialog/Drawer scroll-lock is applied so it targets the root (<html>) element instead of <body>, preventing layout shifts and “white gutter” artifacts when scrollbar-gutter: stable is used on :root.

Changes:

  • Toggle the dialog-open class on document.documentElement (root element) instead of document.body.
  • Update Dialog styles to scope scroll-lock to :root.dialog-open.
  • Adjust unit tests and the v6 migration note to reflect the root-element scroll lock (and bump bundlewatch thresholds).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
js/src/dialog-base.js Moves dialog-open toggling from <body> to <html> for correct scroll-lock behavior with scrollbar-gutter.
scss/_dialog.scss Changes scroll-lock selector from .dialog-open to :root.dialog-open.
js/tests/unit/dialog.spec.js Updates expectations/cleanup to check root element for dialog-open.
js/tests/unit/drawer.spec.js Updates expectations/cleanup to check root element for dialog-open.
site/src/content/docs/guides/migration.mdx Updates migration guidance to document root-element scroll prevention.
.bundlewatch.config.json Adjusts bundle size budgets in anticipation of output changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread js/src/dialog-base.js
Comment on lines 161 to 167
if (preventBodyScroll) {
document.body.classList.add(CLASS_NAME_OPEN)
// Lock scroll on the root element (not <body>) so it lands on the same
// element that carries `scrollbar-gutter: stable`. Co-locating them keeps
// the gutter reserved while the scrollbar is hidden, so the page doesn't
// shift (and the ::backdrop covers the gutter instead of leaving a strip).
document.documentElement.classList.add(CLASS_NAME_OPEN)
}
Comment thread scss/_dialog.scss
Comment on lines 61 to 68
@layer components {
// Prevent body scroll when dialog is open
.dialog-open {
// Prevent page scroll when a dialog is open. Applied to the root element so
// `overflow: hidden` sits on the same element as `scrollbar-gutter: stable`
// (see _root.scss): the gutter stays reserved while the scrollbar is hidden,
// so the page doesn't shift when a dialog opens.
:root.dialog-open {
overflow: hidden;
}
@mdo mdo added this to v6.0.0 Jun 27, 2026
@github-project-automation github-project-automation Bot moved this to Inbox in v6.0.0 Jun 27, 2026
@mdo mdo merged commit 4d08db1 into v6-dev Jun 27, 2026
13 checks passed
@mdo mdo deleted the mdo/dialog-root-scroll-lock branch June 27, 2026 03:15
mdo added a commit that referenced this pull request Jun 27, 2026
…42575)

* Tests: assert dialog-open on documentElement in dispose-while-open specs

The dispose-while-open specs added in #42544 asserted the scroll-lock
class on document.body, but #42545 moved the lock to the root element
(documentElement). The two changes were each green in isolation but
broke once both landed on v6-dev. Align these specs with the rest of
the suite, which already asserts on documentElement.

* Build: bump bundlewatch thresholds for cumulative JS growth on v6-dev

bootstrap.bundle.js (83.15KB) and bootstrap.js (54.39KB) now exceed the
prior thresholds after the dialog/drawer/tooltip/popover/menu fixes
landed; each PR bumped only relative to its own base.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Inbox

Development

Successfully merging this pull request may close these issues.

2 participants